Простые приложения на C#
Простые приложения на C#
Язык программирования C# сочетает в себе мощь объектно-ориентированного подхода, строгую типизацию и высокую производительность. Консольные приложения на этом языке служат идеальной основой для понимания фундаментальных концепций: работы с памятью, управления потоками выполнения, обработки исключений и взаимодействия с операционной системой. Ниже представлены примеры реализации типовых утилит, демонстрирующих ключевые возможности платформы .NET.
Генератор случайных паролей
Генерация надежных паролей требует комбинации различных символов из заданных наборов. В C# для этого используют класс Random или более современный System.RandomNumberGenerator. Работа со строками осуществляется через методы преобразования массивов символов в строки.
Пример кода
using System;
using System.Linq;
using System.Security.Cryptography;
class PasswordGenerator
{
static void Main()
{
int length = 16;
string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
// Использование криптографически стойкого генератора
var randomBytes = new byte[length];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
char[] passwordChars = new char[length];
for (int i = 0; i < length; i++)
{
int index = randomBytes[i] % chars.Length;
passwordChars[i] = chars[index];
}
string password = new string(passwordChars);
Console.WriteLine($"Сгенерированный пароль: {password}");
}
}
Разбор логики
Код создает массив байтов фиксированного размера. Метод GetBytes заполняет этот массив криптографически стойкими случайными значениями. Цикл перебирает каждый байт, вычисляет остаток от деления на длину набора допустимых символов и выбирает соответствующий символ. Результат собирается в строку и выводится в консоль. Использование RandomNumberGenerator предпочтительнее обычного Random, так как он обеспечивает непредсказуемость значений, необходимую для безопасности.
Сортировщик текстового файла
Работа с файловой системой включает чтение содержимого, обработку данных в памяти и запись результата обратно. Классы File и StreamReader/StreamWriter предоставляют удобный интерфейс для этих операций. Список строк сортируется методом Sort, который использует алгоритм быстрой сортировки.
Пример кода
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class TextSorter
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Использование: SortText.exe <input_file> <output_file>");
return;
}
string inputFile = args[0];
string outputFile = args[1];
if (!File.Exists(inputFile))
{
Console.WriteLine("Ошибка: Входной файл не найден.");
return;
}
// Чтение всех строк из файла
List<string> lines = File.ReadAllLines(inputFile).ToList();
// Сортировка строк по алфавиту (независимо от регистра)
lines.Sort((a, b) => string.Compare(a, b, StringComparison.OrdinalIgnoreCase));
// Запись отсортированных строк
File.WriteAllLines(outputFile, lines);
Console.WriteLine($"Файл '{outputFile}' успешно создан с {lines.Count} строками.");
}
}
Разбор логики
Метод ReadAllLines загружает все строки файла в список. Лямбда-выражение внутри метода Sort определяет правило сравнения: строки сравниваются без учета регистра. После сортировки метод WriteAllLines записывает обновленный список в новый файл. Обработка ошибок проверяет существование входного файла перед началом работы.
Консольный калькулятор
Арифметические операции реализуются через условные конструкции. Парсинг пользовательского ввода требует разделения строки на компоненты. Обработка исключений защищает программу от некорректных данных.
Пример кода
using System;
class Calculator
{
static void Main()
{
Console.Write("Введите первое число: ");
if (!double.TryParse(Console.ReadLine(), out double num1))
{
Console.WriteLine("Ошибка: Неверный формат первого числа.");
return;
}
Console.Write("Выберите операцию (+, -, *, /): ");
string operation = Console.ReadLine();
Console.Write("Введите второе число: ");
if (!double.TryParse(Console.ReadLine(), out double num2))
{
Console.WriteLine("Ошибка: Неверный формат второго числа.");
return;
}
double result = 0;
bool validOperation = true;
switch (operation)
{
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 == 0)
{
Console.WriteLine("Ошибка: Деление на ноль невозможно.");
validOperation = false;
}
else
{
result = num1 / num2;
}
break;
default:
Console.WriteLine("Ошибка: Неизвестная операция.");
validOperation = false;
break;
}
if (validOperation)
{
Console.WriteLine($"Результат: {result}");
}
}
}
Разбор логики
Программа последовательно запрашивает данные у пользователя. Метод TryParse пытается преобразовать введенную строку в число типа double. Если преобразование не удается, программа сообщает об ошибке и завершается. Операция выбирается через конструкцию switch. При делении выполняется проверка делителя на ноль. Результат выводится только если операция выполнена корректно.
Трекер задач в формате JSON
Сертификация и десериализация объектов позволяют хранить сложные структуры данных в текстовом виде. Библиотека System.Text.Json является стандартным инструментом для работы с JSON в современных версиях .NET.
Пример кода
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
class TaskTracker
{
class TaskItem
{
public int Id { get; set; }
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
static void Main()
{
string filePath = "tasks.json";
List<TaskItem> tasks = LoadTasks(filePath);
Console.WriteLine("Добавьте новую задачу (введите описание, 'q' для выхода):");
while (true)
{
string input = Console.ReadLine();
if (input.ToLower() == "q") break;
int id = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
tasks.Add(new TaskItem { Id = id, Description = input, IsCompleted = false });
SaveTasks(tasks, filePath);
Console.WriteLine($"Задача #{id} добавлена.");
}
Console.WriteLine("\nВсе задачи:");
foreach (var task in tasks)
{
string status = task.IsCompleted ? "[x]" : "[ ]";
Console.WriteLine($"{status} {task.Id}: {task.Description}");
}
}
static List<TaskItem> LoadTasks(string path)
{
if (!File.Exists(path)) return new List<TaskItem>();
string json = File.ReadAllText(path);
return JsonSerializer.Deserialize<List<TaskItem>>(json) ?? new List<TaskItem>();
}
static void SaveTasks(List<TaskItem> tasks, string path)
{
string json = JsonSerializer.Serialize(tasks);
File.WriteAllText(path, json);
}
}
Разбор логики
Класс TaskItem описывает структуру одной записи. Метод LoadTasks читает файл и преобразует JSON-строку в список объектов. Если файл отсутствует, возвращается пустой список. Метод SaveTasks выполняет обратное действие: объект сериализуется в JSON и записывается в файл. Цикл while позволяет пользователю добавлять задачи до тех пор, пока не будет введена команда выхода.
Простой HTTP-сервер и клиент
Класс HttpListener позволяет создавать сервера, принимающие запросы. Класс HttpClient используется для отправки запросов к этим серверам. Это базовый пример сетевой коммуникации без использования сторонних фреймворков.
Пример кода Сервера
using System;
using System.Net;
using System.Threading.Tasks;
class SimpleServer
{
static async Task Main()
{
string prefix = "http://localhost:8080/";
HttpListener listener = new HttpListener();
listener.Prefixes.Add(prefix);
listener.Start();
Console.WriteLine("Сервер запущен. Нажмите Ctrl+C для остановки.");
try
{
while (true)
{
HttpListenerContext context = await listener.GetContextAsync();
HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response;
string responseBody = $"Получен запрос: {request.Url.PathAndQuery}";
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseBody);
response.ContentLength64 = buffer.Length;
System.IO.Stream output = response.OutputStream;
await output.WriteAsync(buffer, 0, buffer.Length);
output.Close();
}
}
finally
{
listener.Stop();
}
}
}
Пример кода Клиента
using System;
using System.Net.Http;
using System.Threading.Tasks;
class SimpleClient
{
static async Task Main()
{
using HttpClient client = new HttpClient();
string url = "http://localhost:8080/test";
try
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Ответ сервера: {body}");
}
catch (HttpRequestException e)
{
Console.WriteLine($"Ошибка запроса: {e.Message}");
}
}
}
Разбор логики
Сервер создает экземпляр HttpListener, регистрирует адрес и запускает цикл ожидания запросов. Получив контекст, сервер формирует ответ, конвертирует текст в байты и записывает его в поток вывода. Клиент создает HttpClient, отправляет GET-запрос и ожидает ответ. Метод EnsureSuccessStatusCode выбрасывает исключение при ошибках HTTP (например, 404 или 500). Асинхронные методы await предотвращают блокировку потока во время сетевого ожидания.
Утилита для сканирования директорий
Рекурсивный обход файловых структур реализуется через метод Directory.GetFiles с указанием параметра поиска поддиректорий. Информация о каждом файле доступна через свойства класса FileInfo.
Пример кода
using System;
using System.IO;
class DirectoryScanner
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Укажите путь к директории.");
return;
}
string rootPath = args[0];
if (!Directory.Exists(rootPath))
{
Console.WriteLine("Директория не найдена.");
return;
}
string[] files = Directory.GetFiles(rootPath, "*", SearchOption.AllDirectories);
Console.WriteLine($"Найдено файлов: {files.Length}");
long totalSize = 0;
foreach (string file in files)
{
FileInfo info = new FileInfo(file);
totalSize += info.Length;
// Ограничим вывод первых 10 файлов для краткости
if (Array.IndexOf(files, file) < 10)
{
Console.WriteLine($"{info.Name} ({info.Length} байт)");
}
}
Console.WriteLine($"Общий размер: {totalSize / 1024.0:F2} КБ");
}
}
Разбор логики
Метод GetFiles принимает корневой путь, маску поиска (*) и опцию AllDirectories, которая включает подпапки в поиск. Цикл проходит по каждому файлу, создает объект FileInfo для получения метаданных (размер, имя) и суммирует общий объем. Проверка индекса массива ограничивает вывод на экран для демонстрации.
Скрипт создания резервной копии
Копирование файлов требует проверки существования источника и целевой папки. Метод File.Copy позволяет дублировать файлы, а Directory.CreateDirectory создает отсутствующие каталоги.
Пример кода
using System;
using System.IO;
using System.Diagnostics;
class BackupUtility
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Использование: Backup.exe <source_folder> <destination_folder>");
return;
}
string source = args[0];
string destination = args[1];
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupFolder = Path.Combine(destination, $"Backup_{timestamp}");
if (!Directory.Exists(source))
{
Console.WriteLine("Источник не существует.");
return;
}
Directory.CreateDirectory(backupFolder);
int copiedCount = 0;
string[] files = Directory.GetFiles(source, "*.*", SearchOption.AllDirectories);
foreach (string file in files)
{
string relativePath = file.Substring(source.Length).TrimStart(Path.DirectorySeparatorChar);
string destFile = Path.Combine(backupFolder, relativePath);
string destDir = Path.GetDirectoryName(destFile);
Directory.CreateDirectory(destDir);
File.Copy(file, destFile, true); // true означает перезапись существующих файлов
copiedCount++;
}
Console.WriteLine($"Резервная копия создана в: {backupFolder}");
Console.WriteLine($"Скопировано файлов: {copiedCount}");
}
}
Разбор логики
Программа получает имена исходной и целевой папок. Текущая дата и время используются для формирования уникального имени папки резервной копии. Создается структура папок, повторяющая исходную. Цикл копирует каждый файл, сохраняя относительный путь. Флаг true в методе Copy разрешает перезапись файлов с таким же именем, если они уже существуют.
Мониторинг дискового пространства
Класс DriveInfo предоставляет информацию о физических и логических дисках системы. Вычисления процентов свободного места выполняются путем деления доступного объема на общий.
Пример кода
using System;
using System.IO;
class DiskMonitor
{
static void Main()
{
DriveInfo[] drives = DriveInfo.GetDrives();
Console.WriteLine("Статус дисков:\n");
Console.WriteLine("Диск | Объем (GB) | Свободно (GB) | Занято (%)");
Console.WriteLine("-----------------------------------------------");
foreach (DriveInfo drive in drives)
{
if (drive.DriveType == DriveType.Fixed || drive.DriveType == DriveType.Network)
{
double totalGB = drive.TotalSize / (1024.0 * 1024.0 * 1024.0);
double freeGB = drive.AvailableFreeSpace / (1024.0 * 1024.0 * 1024.0);
double usedPercent = ((totalGB - freeGB) / totalGB) * 100;
string label = drive.Name.TrimEnd(':');
Console.WriteLine($"{label,-4} | {totalGB,10:F2} | {freeGB,10:F2} | {usedPercent,10:F1}%");
}
}
}
}
Разбор логики
Метод GetDrives возвращает массив всех подключенных томов. Цикл фильтрует диски по типу (фиксированные и сетевые), игнорируя приводы CD/DVD. Объемы переводятся из байт в гигабайты. Процент занятого места рассчитывается как разница между общим и свободным объемом, деленная на общий объем. Вывод форматируется с использованием спецификаторов ширины поля.
Парсер URL и проверка доступности ресурса
Разбор ссылок осуществляется через класс Uri. Проверка доступности требует создания HTTP-запроса и анализа кода состояния ответа.
Пример кода
using System;
using System.Net.Http;
class UrlChecker
{
static async Task Main()
{
string[] urls = {
"https://example.com",
"https://nonexistent-site-12345.com",
"http://localhost:8080"
};
using HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);
foreach (string url in urls)
{
try
{
Uri uri = new Uri(url);
Console.WriteLine($"\nURL: {url}");
Console.WriteLine($"Хост: {uri.Host}");
Console.WriteLine($"Порт: {uri.Port}");
Console.WriteLine($"Схема: {uri.Scheme}");
HttpResponseMessage response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Статус: Доступен ({(int)response.StatusCode})");
}
else
{
Console.WriteLine($"Статус: Недоступен ({(int)response.StatusCode})");
}
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка: {ex.Message}");
}
}
}
}
Разбор логики
Массив ссылок содержит тестовые адреса. Цикл создает объект Uri для каждого элемента, извлекая хост, порт и схему. Настройка таймаута предотвращает зависание программы при отсутствии ответа от удаленного сервера. Метод GetAsync отправляет запрос, а свойство IsSuccessStatusCode указывает на успешность операции (код 2xx). Исключения перехватываются для обработки сетевых ошибок.
Конвертер форматов дат
Класс DateTime поддерживает различные форматы представления времени. Метод ToString преобразует дату в строку по заданному шаблону, а ParseExact выполняет обратное действие, проверяя соответствие строки шаблону.
Пример кода
using System;
class DateConverter
{
static void Main()
{
string inputDate = "2025-11-01T14:30:00";
string formatIn = "yyyy-MM-ddTHH:mm:ss";
string formatOut = "dd.MM.yyyy HH:mm";
try
{
DateTime dt = DateTime.ParseExact(inputDate, formatIn, null);
string output = dt.ToString(formatOut);
Console.WriteLine($"Входная строка: {inputDate}");
Console.WriteLine($"Выходная строка: {output}");
Console.WriteLine($"Тип даты: {dt.Kind}");
}
catch (FormatException)
{
Console.WriteLine("Ошибка: Неправильный формат входной строки.");
}
}
}
Разбор логики
Входная строка соответствует ISO 8601. Метод ParseExact строго проверяет соответствие шаблона yyyy-MM-ddTHH:mm:ss. Если формат не совпадает, выбрасывается исключение. Результат преобразуется в формат dd.MM.yyyy HH:mm с помощью метода ToString. Свойство Kind показывает локальное или универсальное время.
Утилита для просмотра запущенных процессов
Класс Process из пространства имен System.Diagnostics позволяет взаимодействовать с процессами операционной системы. Можно получить список всех активных процессов, их идентификаторы и названия.
Пример кода
using System;
using System.Diagnostics;
class ProcessViewer
{
static void Main()
{
Process[] processes = Process.GetProcesses();
Console.WriteLine($"Активных процессов: {processes.Length}\n");
Console.WriteLine("PID | Имя процесса | Время CPU (сек)");
Console.WriteLine("--------------------------------------------------");
// Ограничиваем вывод первыми 20 процессами для читаемости
int count = 0;
foreach (Process proc in processes)
{
if (count >= 20) break;
try
{
string name = proc.ProcessName;
long cpuSeconds = (long)(proc.TotalProcessorTime.TotalSeconds);
int pid = proc.Id;
Console.WriteLine($"{pid,-8} | {name,-18} | {cpuSeconds}");
}
catch (InvalidOperationException)
{
// Процесс завершился до чтения данных
continue;
}
catch (System.ComponentModel.Win32Exception)
{
// Нет прав доступа к процессу
continue;
}
count++;
}
}
}
Разбор логики
Метод GetProcesses возвращает массив всех процессов, запущенных текущим пользователем. Цикл проходит по списку, пытаясь прочитать свойства ProcessName, Id и TotalProcessorTime. Обработка исключений необходима, так как процессы могут завершиться в момент чтения или требовать повышенных привилегий. Ограничение количества выводимых строк делает вывод удобным для восприятия в консоли.
Характерный пример для языка C#
Особенностью C# является использование событий и делегатов для реализации реактивного поведения. Приведенный пример демонстрирует создание простого механизма подписки на события, что характерно для архитектуры приложений на базе .NET.
Пример кода
using System;
// Определение делегата
public delegate void MessageHandler(string message);
// Класс, генерирующий события
public class EventPublisher
{
public event MessageHandler OnMessageReceived;
public void Broadcast(string msg)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Отправлено сообщение: {msg}");
OnMessageReceived?.Invoke(msg);
}
}
// Класс, подписывающийся на события
public class Subscriber
{
public void HandleMessage(string msg)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Получено: {msg}");
}
}
class Program
{
static void Main()
{
EventPublisher publisher = new EventPublisher();
Subscriber sub1 = new Subscriber();
Subscriber sub2 = new Subscriber();
// Подписка на событие
publisher.OnMessageReceived += sub1.HandleMessage;
publisher.OnMessageReceived += sub2.HandleMessage;
// Генерация события
publisher.Broadcast("Привет, мир!");
publisher.Broadcast("C# мощный язык!");
// Отписка от события
publisher.OnMessageReceived -= sub1.HandleMessage;
publisher.Broadcast("Только второй подписчик услышит это.");
}
}
Разбор логики
Делегат MessageHandler определяет сигнатуру метода, который может обрабатывать событие. Событие OnMessageReceived объявляется в классе EventPublisher. Метод Broadcast вызывает обработчики, используя оператор ?.Invoke, что безопасно даже при отсутствии подписчиков. Классы Subscriber реализуют метод обработки. В методе Main происходит регистрация обработчиков через оператор += и удаление через -=. Это паттерн наблюдателя, широко используемый в UI-фреймворках и системах сообщений.